#pragma once

using namespace System;
using namespace System::IO;
using namespace System::Collections::Generic;

#include "NetworkBase.h"
#include "Stimulus.h"
#include "Connection.h"

namespace SpikingNeuronLib
{
	// This class is a reimplementation of the IzhikevichNetwork class using largely the same algorithms
	// but with different data structures in order to increase flexibility
	// e.g. this class supports networks with arbitrary connectivity whereas the IzhikevichNetwork class does not
	// e.g. this class has a new plasticity implementation that includes metaplasticity
	// This class also has additional instrumentation (see for example the OnOneMillisecondTick event call in RunImpl())
	// Note however that the increased flexibility comes at the cost of some performance c.f. the IzhikevichNetwork class
	public ref class CrossbarNetwork : public NetworkBase
	{
	protected:
		int _numConnections;				// total number of connections in the network
		int _numExcitatoryConnections;		// number of excitatory connections in the network
		int _numInhibitoryConnections;		// number of inhibitory connections in the network

		double _maxSynapticWeightHardLimit;			// capped value for upper end of the range of allowable synaptic weights
		double _potentiationAmplitudeBaseline;		// STDP rule constant for LTP window (default = 0.1)
		double _depressionAmplitudeBaseline;		// STDP rule constant for LTD window (default = 0.12)
		bool _plasticityDisabled;					// if set to true, the synaptic weights will not be updated i.e. they will remain at initial values

		// storing current network time in an Int32 -> approx. 24 days network simulation time
		// however using an Int64 just in case there is a need to simulate more than 24 days
		// current implementation will overflow 'sec' (see RunNetwork) after 68 years network simulation time
		// int = long = Int32
		// long long = Int64
		long long _timestamp;						// current network time (simulated seconds + milliseconds)
		array<long long>^ _lastSpikeTime;			// timestamp of last spike for each neuron

		// metaplasticity variables
		// metaplasticity scales are updated once a second
		// potentiation and depression amplitudes can be computed by multiplying the baseline values with the scale value
		double _maxSynapticWeightSoftLimit;						// the metaplastic soft limit for the upper end of the range of allowable synaptic weights
		double _minSynapticWeightSoftLimit;						// the metaplastic soft limit for the lower end of the range of allowable synaptic weights
		double _inertia;										// the overall resistance to weight change (adjusts the sensitivity of the neuron derivative to the weighted average synaptic derivative)
		double _upperWeightLimitResistance;						// the resistance to changes in synaptic weights near the upper limit (max synaptic weight)
		double _lowerWeightLimitResistance;						// the resistance to changes in synaptic weights near the lower limit (zero synaptic weight)
		double _upperWeightLimitPrecision;						// the precision of synaptic weight scaling near the upper limit (max synaptic weight)
		double _lowerWeightLimitPrecision;						// the precision of synaptic weight scaling near the lower limit (zero synaptic weight)
		bool _metaplasticityEnabled;							// if set to true, the LTP/LTD amplitudes for STDP will adjust dynamically
		array<double>^ _potentiationAmplitudeScale;				// the scale value for LTP amplitude for each neuron (if metaplasticity is enabled)
		array<double>^ _depressionAmplitudeScale;				// the scale value for LTD amplitude for each neuron (if metaplasticity is enabled)
		array<double>^ _depressionAmplitudeScalePrevious;		// the LTD scale value at the time of firing of the postsynaptic neuron (if it occurred in the previous second)
		array<double>^ _wsd;									// weighted synaptic derivative for each neuron
		array<double>^ _neuronDerivative;						// a range-limited weighted average synaptic derivative for each neuron (averaged over the synaptic inputs) where range is controlled by a sigmoid-like function
		array<double>^ _LTPLookup;								// a fixed table of values that model the STDP exponential for LTP
		array<double>^ _LTDLookup;								// a fixed table of values that model the STDP exponential for LTD
		array<int>^ _connectionsPerNeuron;						// the number of synaptic inputs to each postsynaptic neuron

		double _foregroundStimulationLevel;	// intensity of external stimulation
		double _backgroundStimulationLevel;	// intensity of background stimulation (defaults to 20)
		bool _labelEvents;					// if set to true, the firing events generated by the network are labeled as background, foreground or internal (see GetFiringsData)

		List<Connection^>^ _singles;		// accumulator for connections added one-by-one
		array<Connection^>^ _connections;	// stores connection properties for all connections in the network
		array<int, 2>^ _crossbar;			// connectivity between pre- and post-synaptic neurons:
											// dimension 1 = pre-synaptic; dimension 2 = post-synaptic
											// cell value is offset into _connections array
											// cell value of zero means no connection

		array<array<int>^, 2>^ _connectionsWithDelay;	// all the connection offsets for pre-synaptic neuron i with delay j
														// dimension 1 = pre-synaptic neuron, dimension 2 = the axonal delay (1 to _maxDelay inclusive)

		array<FiringEvent>^ _firings;		// the firing stack

		// external stimulus firing times for input stimulus and background stimulus
		// for each msec timeslot, the array of neuron indices that fire in that timeslot
		array<array<int>^>^ _stimulusFiringData;	// neuron firing times for the current second for the input stimulus
		array<array<int>^>^ _backgroundFiringData;	// neuron firing times for the current second for the background stimulus

		void Init(int numExcitatoryNeurons, int numInhibitoryNeurons, int maxDelay, unsigned int seed);
		void PrepareConnections(List<Connection^>^ connections);
		void ResizeFiringStack(int newSize);
		int FindIndex(array<double>^ data, double value);
		inline void UpdateSynapticIntegral(int offset, int t);

		List<Connection^>^ CreateFixedConnectionList(int numExcitatory, int numInhibitory, int synapsesPerNeuron, int maxDelay, double defaultExcitatoryWeight, double defaultInhibitoryWeight);

		virtual int RunImpl(int totalSeconds, PatternGenerator^ backgroundPattern, IStimulus^ inputPattern) override;
		virtual int RunImpl(int startSeconds, int endSeconds, PatternGenerator^ backgroundPattern, IStimulus^ inputPattern) override;
		int RunNetwork(int startSeconds, int endSeconds, PatternGenerator^ backgroundPattern, IStimulus^ inputPattern);

	public:
		// Properties

		// A list of all connections
		property List<Connection^>^ Connections
		{
		public:
			virtual List<Connection^>^ get() override
			{
				List<Connection^>^ connections = gcnew List<Connection^>(_connections->Length - 1);
				for (int i = 1; i < _connections->Length; i++)
				{
					connections->Add(_connections[i]);
				}

				return connections;
			}
		}

		// Estimated connections per neuron
		// Actually an average of the input connections per neuron
		property int ConnectionsPerNeuron
		{
		public:
			virtual int get() override { return (int) System::Linq::Enumerable::Average(_connectionsPerNeuron); }
		}

		// The total number of connections
		property int TotalConnections
		{
		public:
			virtual int get() override { return _numConnections; }
		}

		// The number of excitatory connections
		property int ExcitatoryConnections
		{
		public:
			virtual int get() override { return _numExcitatoryConnections; }
		}

		// The number of inhibitory connections
		property int InhibitoryConnections
		{
		public:
			virtual int get() override { return _numInhibitoryConnections; }
		}

		// A 2D array for grid networks, indexed by pre-synaptic neuron i and delay j
		// Allows looking up the connection offsets for neuron i that have delay j
		// i.e. retrieval of the connection indices for each combination of neuron and delay
		property array<array<int>^, 2>^ ConnectionsByNeuronAndDelay
		{
		public:
			array<array<int>^, 2>^ get() { return _connectionsWithDelay; }
		}

		// The input potential for each foreground (external) stimulus
		property double ForegroundStimulationLevel
		{
		public:
			double get() { return _foregroundStimulationLevel; }
			void set(double value) { _foregroundStimulationLevel = value; }
		}

		// The input potential for each background stimulus
		property double BackgroundStimulationLevel
		{
		public:
			double get() { return _backgroundStimulationLevel; }
			void set(double value) { _backgroundStimulationLevel = value; }
		}

		// If true then no plasticity changes are allowed
		property bool PlasticityDisabled
		{
		public:
			bool get() { return _plasticityDisabled; }
			void set(bool value) { _plasticityDisabled = value; }
		}

		// If true then LTP/LTD amplitudes for STDP will adjust dynamically
		property bool MetaplasticityEnabled
		{
		public:
			bool get() { return _metaplasticityEnabled; }
			void set(bool value) { _metaplasticityEnabled = value; }
		}

		// Label firing events as Foreground, Background etc
		property bool LabelEvents
		{
		public:
			bool get() { return _labelEvents; }
			void set(bool value) { _labelEvents = value; }
		}

		// Global baseline value for LTP amplitude (across all neurons)
		property double PotentiationAmplitudeBaseline
		{
		public:
			double get() { return _potentiationAmplitudeBaseline; }
			void set(double value) { _potentiationAmplitudeBaseline = value; }
		}

		// Global baseline value for LTD amplitude (across all neurons)
		property double DepressionAmplitudeBaseline
		{
		public:
			double get() { return _depressionAmplitudeBaseline; }
			void set(double value) { _depressionAmplitudeBaseline = value; }
		}

		// The capped value for the upper end of the range of allowable synaptic weights
		property double MaxSynapticWeightHardLimit
		{
		public:
			double get() { return _maxSynapticWeightHardLimit; }
			void set(double value) { _maxSynapticWeightHardLimit = value; }
		}

		// The metaplastic soft limit for the lower end of the range of allowable synaptic weights
		// Weight values close to this limit are increasingly resisted
		// There is also a hard limit of zero that provides a cap on the minimum allowed weights
		property double MetaplasticityMinSynapticWeight
		{
		public:
			double get() { return _minSynapticWeightSoftLimit; }
			void set(double value) { _minSynapticWeightSoftLimit = value; }
		}

		// The metaplastic soft limit for the upper end of the range of allowable synaptic weights
		// Weight values close to this limit are increasingly resisted
		// There is also a hard limit that provides a cap on the maximum allowed weights
		property double MetaplasticityMaxSynapticWeight
		{
		public:
			double get() { return _maxSynapticWeightSoftLimit; }
			void set(double value) { _maxSynapticWeightSoftLimit = value; }
		}

		// The LTP amplitude scale for each neuron (if metaplasticity is disabled this is the same for all neurons)
		property array<double>^ MetaplasticityPotentiationAmplitudeScales
		{
		public:
			array<double>^ get() { return _potentiationAmplitudeScale; }
		}

		// The LTD amplitude scale for each neuron (if metaplasticity is disabled this is the same for all neurons)
		property array<double>^ MetaplasticityDepressionAmplitudeScales
		{
		public:
			array<double>^ get() { return _depressionAmplitudeScale; }
		}

		// The LTP amplitude for each neuron (if metaplasticity is disabled this is the same for all neurons)
		property array<double>^ MetaplasticityPotentiationAmplitudes
		{
		public:
			array<double>^ get()
			{
				// scale the baseline amplitudes for each neuron
				array<double>^ result = gcnew array<double>(_potentiationAmplitudeScale->Length);
				for (int i = 0; i < _potentiationAmplitudeScale->Length; i++)
				{
					result[i] = _potentiationAmplitudeScale[i] * _potentiationAmplitudeBaseline;
				}

				return result;
			}
		}

		// The LTD amplitude for each neuron (if metaplasticity is disabled this is the same for all neurons)
		property array<double>^ MetaplasticityDepressionAmplitudes
		{
		public:
			array<double>^ get()
			{
				// scale the baseline amplitudes for each neuron
				array<double>^ result = gcnew array<double>(_depressionAmplitudeScale->Length);
				for (int i = 0; i < _depressionAmplitudeScale->Length; i++)
				{
					result[i] = _depressionAmplitudeScale[i] * _depressionAmplitudeBaseline;
				}

				return result;
			}
		}

		// The weighted synaptic derivative for each neuron
		property array<double>^ MetaplasticityWeightedSynapticDerivative
		{
		public:
			array<double>^ get() { return _wsd; }
		}

		// The neuron derivative for each neuron
		// Derived from the weighted average synaptic derivative for each neuron
		property array<double>^ MetaplasticityNeuronDerivative
		{
		public:
			array<double>^ get() { return _neuronDerivative; }
		}

		// The average synaptic derivative for each neuron
		// (the derivative values averaged over all the synaptic inputs for each post-synaptic neuron)
		property array<double>^ MetaplasticityUnweightedAverageSynapticDerivative
		{
		public:
			array<double>^ get()
			{
				array<double>^ result = gcnew array<double>(_totalNeurons);
				for (int offset = 1; offset < _connections->Length; offset++)
				{
					Connection^ connection = _connections[offset];

					result[connection->PostNeuron] += connection->Derivative;
				}

				for (int i = 0; i < _totalNeurons; i++)
				{
					// for neurons with synaptic inputs...
					if (_connectionsPerNeuron[i])
					{
						result[i] /= _connectionsPerNeuron[i];
					}
					else
					{
						result[i] = 0.0;
					}
				}

				return result;
			}
		}

		// The resistance to changes in synaptic weights near the upper limit (max synaptic weight)
		property double MetaplasticityUpperWeightLimitResistance
		{
		public:
			double get() { return _upperWeightLimitResistance; }
			void set(double value) { _upperWeightLimitResistance = value; }
		}

		// The resistance to changes in synaptic weights near the lower limit (zero synaptic weight)
		property double MetaplasticityLowerWeightLimitResistance
		{
		public:
			double get() { return _lowerWeightLimitResistance; }
			void set(double value) { _lowerWeightLimitResistance = value; }
		}

		// The precision of synaptic weight scaling near the upper limit (max synaptic weight)
		property double MetaplasticityUpperWeightLimitPrecision
		{
		public:
			double get() { return _upperWeightLimitPrecision; }
			void set(double value) { _upperWeightLimitPrecision = value; }
		}

		// The precision of synaptic weight scaling near the lower limit (zero synaptic weight)
		property double MetaplasticityLowerWeightLimitPrecision
		{
		public:
			double get() { return _lowerWeightLimitPrecision; }
			void set(double value) { _lowerWeightLimitPrecision = value; }
		}

		// The overall resistance to weight change (adjusts the sensitivity of the neuron derivative to the weighted average synaptic derivative)
		property double MetaplasticityInertia
		{
		public:
			double get() { return _inertia; }
			void set(double value) { _inertia = value; }
		}

		// The number of synaptic inputs to each postsynaptic neuron
		property array<int>^ SynapticInputsPerNeuron
		{
		public:
			array<int>^ get() { return _connectionsPerNeuron; }
		}

		// Constructors and Destructors
		CrossbarNetwork();
		CrossbarNetwork(int numExcitatoryNeurons, int numInhibitoryNeurons, int maxDelay);
		CrossbarNetwork(int numExcitatoryNeurons, int numInhibitoryNeurons, int maxDelay, unsigned int seed);
		~CrossbarNetwork();

		// Methods

		// Create a full copy of the current network
		virtual CrossbarNetwork^ CrossbarNetwork::Clone();

		// Copy values between networks
		virtual void CrossbarNetwork::CopyTo(CrossbarNetwork^ network);

		// Get the firings data
		// This method must be called from within a OneSecondTickBeforeCleanup event handler
		// as code after this event callback cleans up the firing stack
		virtual array<FiringEvent>^ GetFiringsData(bool includeOverflow) override;

		// Save the firings data
		virtual void SaveFiringsData(String^ filePath, bool includeOverflow) override;
		virtual void LoadFiringsData(String^ filePath) override;

		// Save or load the complete network state
		// Network state must not be saved from within a OneSecondTickBeforeCleanup event handler:
		// the cleanup code at the end of each second must be called first or the network state will be corrupt
		// Save the network state from within a OneSecondTickAfterCleanup event handler instead
		virtual void SaveNetworkState(String^ filePath) override;
		virtual void LoadNetworkState(String^ filePath) override;
		void SaveNetworkState(StreamWriter^ sw);
		void LoadNetworkState(StreamReader^ sr);

		// Save the connectivity data
		void SaveConnectivity(String^ filePath);
		void LoadConnectivity(String^ filePath);

		// Add a fixed number of connections per neuron
		// For creation of fixed networks only
		void AddFixedConnectionList(int synapsesPerNeuron);
		void AddFixedConnectionList(int synapsesPerNeuron, double defaultExcitatoryWeight, double defaultInhibitoryWeight);

		// Add arbitrary connections
		void AddConnections(List<Connection^>^ connections);	// create a fixed or variable network; use CreateFixedConnectionList() to create a fixed network
		void AddConnection(Connection^ connection);				// add a single connection from the specified neuron

		// For grid networks, get all of the connections from the specified pre-synaptic neuron that have the specified delay
		array<int>^ GetConnectionsForNeuronAndDelay(int neuronIndex, int delay);

		// Set all connection weights to a random value up to the specified maximum weight
		// Inhibitory connections are set to the negative of each random value
		void RandomizeWeights(double maxWeight);

		// Perturb weight values up or down in a random direction and by a random amount within the specified range
		void PerturbWeights(int perturbationRange);

		// Set all excitatory and inhibitory connection weights to the same values
		void SetUniformConnectionWeights(double excitatoryWeight, double inhibitoryWeight);
	};
}
